package com.googlecode.spoluyan.myshows;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.gson.reflect.TypeToken;
import com.googlecode.spoluyan.myshows.Defines.Command;
import com.googlecode.spoluyan.myshows.Defines.Gender;
import com.googlecode.spoluyan.myshows.Defines.Status;
import com.googlecode.spoluyan.myshows.data.Episode;
import com.googlecode.spoluyan.myshows.data.Genre;
import com.googlecode.spoluyan.myshows.data.News;
import com.googlecode.spoluyan.myshows.data.Profile;
import com.googlecode.spoluyan.myshows.data.ProfileShow;
import com.googlecode.spoluyan.myshows.data.SearchByFileResult;
import com.googlecode.spoluyan.myshows.data.SearchShow;
import com.googlecode.spoluyan.myshows.data.ShowWithEpisodes;
import com.googlecode.spoluyan.myshows.data.TopRatedShow;

public class Client implements IClient {
    private String login;
    private String password;
    private Set<String> cookies;

    public Client(String login, String password, boolean isMD5Password) throws MyShowsClientException {
	if (Utils.isEmptyString(login) || Utils.isEmptyString(password)) {
	    throw new MyShowsClientException(Defines.ErrorMessage.LOGIN_OR_PASSWORD_IS_EMPTY);
	}

	if (!isMD5Password) {
	    try {
		password = Utils.getMD5(password);
	    } catch (NoSuchAlgorithmException e) {
		throw new MyShowsClientException(Defines.ErrorMessage.MD5_IS_NOT_SUPPORTED);
	    } catch (UnsupportedEncodingException e) {
		throw new MyShowsClientException(Defines.ErrorMessage.ENCODING_IS_NOT_SUPPORTED);
	    }
	}

	this.login = login;
	this.password = password;

	authorize(login, password);
    }

    private String doRequestWithAuthCheck(Map<String, String> queryParams, String... restParams)
	    throws MyShowsClientException {
	try {
	    String result = doRequestWithoutAuthCheck(queryParams, restParams);

	    if (result == null) {
		authorize(login, password);
		result = Utils.doRequest(cookies, queryParams, restParams);
	    }

	    return result;
	} catch (UnsupportedEncodingException e) {
	    throw new MyShowsClientException(Defines.ErrorMessage.ENCODING_IS_NOT_SUPPORTED);
	}
    }

    private String doRequestWithoutAuthCheck(Map<String, String> queryParams, String... restParams)
	    throws MyShowsClientException {
	try {
	    return Utils.doRequest(cookies, queryParams, restParams);
	} catch (UnsupportedEncodingException e) {
	    throw new MyShowsClientException(Defines.ErrorMessage.ENCODING_IS_NOT_SUPPORTED);
	}
    }

    private void authorize(String login, String passwordMD5) throws MyShowsClientException {
	cookies = new HashSet<String>();

	Map<String, String> queryParams = new HashMap<String, String>();
	queryParams.put(Defines.Parameter.LOGIN, login);
	queryParams.put(Defines.Parameter.PASSWORD, passwordMD5);
	try {
	    URL url = new URL(Utils.generateURL(queryParams, Defines.Path.PROFILE, Defines.Path.LOGIN));
	    URLConnection conn = url.openConnection();
	    conn.connect();

	    String headerName = null;
	    for (int i = 1; (headerName = conn.getHeaderFieldKey(i)) != null; i++) {
		if (Defines.COOKIE_HEADER.equals(headerName)) {
		    cookies.add(conn.getHeaderField(i));
		}
	    }

	    byte[] result = Utils.getDataFromOpenedConnection(conn);
	    if (result == null) {
		throw new MyShowsClientException(Defines.ErrorMessage.LOGIN_OR_PASSWORD_IS_INCORRECT);
	    }
	} catch (UnsupportedEncodingException e) {
	    throw new MyShowsClientException(Defines.ErrorMessage.ENCODING_IS_NOT_SUPPORTED);
	} catch (MalformedURLException e) {
	    throw new MyShowsClientException(Defines.ErrorMessage.INCORRECT_URL + e.getMessage());
	} catch (IOException e) {
	    throw new MyShowsClientException(Defines.ErrorMessage.UNABLE_TO_CONNECT_TO);
	}
    }

    @Override
    public Set<ProfileShow> getShows() throws MyShowsClientException {
	return Utils.setFromJSON(doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.SHOWS),
		new TypeToken<Map<String, ProfileShow>>() {
		}.getType());
    }

    @Override
    public Set<Episode> getWatchedEpisodes(Long showId) throws MyShowsClientException {
	if (showId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}
	return Utils.setFromJSON(
		doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.SHOWS,
			String.valueOf(showId.longValue()), Defines.SLASH), new TypeToken<Map<String, Episode>>() {
		}.getType());
    }

    @Override
    public Set<Episode> getUnwatchedEpisodes() throws MyShowsClientException {
	return Utils.setFromJSON(
		doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.UNWATCHED),
		new TypeToken<Map<String, Episode>>() {
		}.getType());
    }

    @Override
    public Set<Episode> getNextEpisodes() throws MyShowsClientException {
	return Utils.setFromJSON(
		doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.NEXT),
		new TypeToken<Map<String, Episode>>() {
		}.getType());
    }

    @Override
    public void checkEpisode(Long episodeId, Short rating) throws MyShowsClientException {
	if (episodeId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.EPISODE_ID_CAN_NOT_BE_NULL);
	}

	Map<String, String> queryParams = new HashMap<String, String>();
	if (rating != null) {
	    short r = rating.shortValue();
	    if (r < 1 || r > 5) {
		throw new MyShowsClientException(Defines.ErrorMessage.INCORRECT_RATING);
	    }
	    queryParams.put(Defines.Parameter.RATING, String.valueOf(r));
	}
	doRequestWithAuthCheck(queryParams, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.CHECK,
		String.valueOf(episodeId.longValue()));
    }

    @Override
    public void uncheckEpisode(Long episodeId) throws MyShowsClientException {
	if (episodeId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.EPISODE_ID_CAN_NOT_BE_NULL);
	}
	doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.UNCHECK,
		String.valueOf(episodeId.longValue()));
    }

    @Override
    public void syncEpisodesFull(Long showId, Long[] episodesIds) throws MyShowsClientException {
	if (showId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (episodesIds == null || episodesIds.length == 0) {
	    return;
	}

	Map<String, String> queryParams = new HashMap<String, String>();
	queryParams.put(Defines.Parameter.EPISODES, Utils.longArrayToParamString(episodesIds));
	doRequestWithAuthCheck(queryParams, Defines.Path.PROFILE, Defines.Path.SHOWS,
		String.valueOf(showId.longValue()), Defines.SLASH, Defines.Path.SYNC);
    }

    @Override
    public void syncEpisodesDelta(Long showId, Long[] check, Long[] uncheck) throws MyShowsClientException {
	if (showId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (check == null || check.length == 0 || uncheck == null || uncheck.length == 0) {
	    throw new MyShowsClientException(Defines.ErrorMessage.CHECK_AND_UNCKECK_CAN_NOT_BE_NULL);
	}

	Map<String, String> queryParams = new HashMap<String, String>();
	queryParams.put(Defines.Parameter.CHECK, Utils.longArrayToParamString(check));
	queryParams.put(Defines.Parameter.UNCHECK, Utils.longArrayToParamString(uncheck));
	doRequestWithAuthCheck(queryParams, Defines.Path.PROFILE, Defines.Path.SHOWS,
		String.valueOf(showId.longValue()), Defines.SLASH, Defines.Path.EPISODES_WITH_PARAMS);
    }

    @Override
    public void updateShowStatus(Long showId, Status status) throws MyShowsClientException {
	if (showId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (status == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.STATUS_CAN_NOT_BE_NULL);
	}

	doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.SHOWS, String.valueOf(showId.longValue()),
		Defines.SLASH, status.getName());
    }

    @Override
    public void updateShowRating(Long showId, Short rating) throws MyShowsClientException {
	if (showId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (rating == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.INCORRECT_RATING);
	}

	short r = rating.shortValue();
	if (r < 1 || r > 5) {
	    throw new MyShowsClientException(Defines.ErrorMessage.INCORRECT_RATING);
	}

	doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.SHOWS, String.valueOf(showId.longValue()),
		Defines.SLASH, Defines.Path.RATE, String.valueOf(r));
    }

    @Override
    public void updateEpisodeRating(Long episodeId, Short rating) throws MyShowsClientException {
	if (episodeId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (rating == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.INCORRECT_RATING);
	}

	short r = rating.shortValue();
	if (r < 1 || r > 5) {
	    throw new MyShowsClientException(Defines.ErrorMessage.INCORRECT_RATING);
	}

	doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.RATE, String.valueOf(r),
		Defines.SLASH, String.valueOf(episodeId.longValue()));
    }

    @Override
    public Set<Long> getFavoritesEpisodesList() throws MyShowsClientException {
	return Utils.jsonStringArrayToLongSet(doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES,
		Defines.Path.FAVORITES, Defines.Path.LIST));
    }

    @Override
    public void updateFavoritesEpisodesList(Command cmd, Long episodeId) throws MyShowsClientException {
	if (episodeId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (cmd == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.COMMAND_CAN_NOT_BE_NULL);
	}

	doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.FAVORITES,
		cmd.getName(), Defines.SLASH, String.valueOf(episodeId.longValue()));
    }

    @Override
    public Set<Long> getIgnoredEpisodesList() throws MyShowsClientException {
	return Utils.jsonStringArrayToLongSet(doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES,
		Defines.Path.IGNORED, Defines.Path.LIST));
    }

    @Override
    public void updateIgnoredEpisodesList(Command cmd, Long episodeId) throws MyShowsClientException {
	if (episodeId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	if (cmd == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.COMMAND_CAN_NOT_BE_NULL);
	}

	doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.EPISODES, Defines.Path.IGNORED, cmd.getName(),
		Defines.SLASH, String.valueOf(episodeId.longValue()));
    }

    @Override
    public Map<String, Set<News>> getFriendsNews() throws MyShowsClientException {
	return Utils.fromJSON(doRequestWithAuthCheck(null, Defines.Path.PROFILE, Defines.Path.NEWS),
		new TypeToken<Map<String, Set<News>>>() {
		}.getType());
    }

    @Override
    public Set<SearchShow> search(String query) throws MyShowsClientException {
	if (Utils.isEmptyString(query)) {
	    return Collections.emptySet();
	}

	Map<String, String> queryParams = new HashMap<String, String>();
	queryParams.put(Defines.Parameter.QUERY, query);

	return Utils.setFromJSON(doRequestWithoutAuthCheck(queryParams, Defines.Path.SHOWS, Defines.Path.SEARCH),
		new TypeToken<Map<String, SearchShow>>() {
		}.getType());
    }

    @Override
    public SearchByFileResult searchByFile(String fileName) throws MyShowsClientException {
	if (Utils.isEmptyString(fileName)) {
	    return null;
	}
	Map<String, String> queryParams = new HashMap<String, String>();
	queryParams.put(Defines.Parameter.QUERY, fileName);
	return Utils.fromJSON(
		doRequestWithoutAuthCheck(queryParams, Defines.Path.SHOWS, Defines.Path.SEARCH, Defines.Path.FILE),
		SearchByFileResult.class);
    }

    @Override
    public ShowWithEpisodes getShowInformation(Long showId) throws MyShowsClientException {
	if (showId == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.SHOW_ID_CAN_NOT_BE_NULL);
	}

	return Utils.fromJSON(doRequestWithoutAuthCheck(null, Defines.Path.SHOWS, String.valueOf(showId.longValue())),
		ShowWithEpisodes.class);
    }

    @Override
    public Set<Genre> getGenres() throws MyShowsClientException {
	return Utils.setFromJSON(doRequestWithoutAuthCheck(null, Defines.Path.GENRES),
		new TypeToken<Map<String, Genre>>() {
		}.getType());
    }

    @Override
    public Set<TopRatedShow> getShowsRating(Gender gender) throws MyShowsClientException {
	if (gender == null) {
	    throw new MyShowsClientException(Defines.ErrorMessage.GENDER_CAN_NOT_BE_NULL);
	}
	return Utils.fromJSON(
		doRequestWithoutAuthCheck(null, Defines.Path.SHOWS, Defines.Path.TOP, gender.getName(), Defines.SLASH),
		new TypeToken<Set<TopRatedShow>>() {
		}.getType());
    }

    @Override
    public Profile getUsersProfile(String login) throws MyShowsClientException {
	if (Utils.isEmptyString(login)) {
	    return null;
	}
	return Utils.fromJSON(doRequestWithoutAuthCheck(null, Defines.Path.PROFILE, login), Profile.class);
    }
}
